﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Windows.Forms;

using VolgaTransTelecomClient.Properties;

namespace VolgaTransTelecomClient
{
    internal class TrafficChecker : IDisposable
    {
        #region Fields
        private NotifyIcon _notification;
        private Timer _timer;
        private List<string> _trustedIssuers;
        private string _userName;
        private string _password;
        private string _proxyUserName;
        private string _proxyPassword;
        #endregion

        #region Methods
        /// <summary>
        /// Initializes this instance.
        /// </summary>
        internal void Initialize()
        {
            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
            _trustedIssuers = Settings.Default.TrustedIssuers ?? new List<string>();
            if (!string.IsNullOrEmpty(Settings.Default.UserName))
            {
                _userName = Security.DecryptValue(Settings.Default.UserName);
            }
            if (!string.IsNullOrEmpty(Settings.Default.Password))
            {
                _password = Security.DecryptValue(Settings.Default.Password);
            }
            if (!string.IsNullOrEmpty(Settings.Default.ProxyUserName))
            {
                _proxyUserName = Security.DecryptValue(Settings.Default.ProxyUserName);
            }
            if (!string.IsNullOrEmpty(Settings.Default.ProxyPassword))
            {
                _proxyPassword = Security.DecryptValue(Settings.Default.ProxyPassword);
            }
            _notification = new NotifyIcon();
            _notification.Icon = new Icon("icon.ico");
            _notification.Visible = true;
            _notification.ContextMenuStrip = new ContextMenuStrip();
            _notification.ContextMenuStrip.Items.Add("Проверить сейчас", null, new EventHandler(NotificationMenuGetDataNow_Click));
            _notification.ContextMenuStrip.Items.Add("Выход", null, new EventHandler(NotificationMenuExit_Click));
            UpdateIncomingTrafficAmount();
            _timer = new Timer();
            _timer.Interval = Settings.Default.UpdateInterval * 60 * 1000;
            _timer.Tick += new EventHandler(Timer_Tick);
            _timer.Start();
        }

        /// <summary>
        /// Validates the server certificate.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="certification">The certification.</param>
        /// <param name="chain">The chain.</param>
        /// <param name="sslPolicyErrors">The SSL policy errors.</param>
        /// <returns>Result of certificate validation.</returns>
        private bool ValidateServerCertificate(object sender, X509Certificate certification, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            bool result = false;
            string issuer = certification.Issuer;
            foreach (string trustedIssuer in _trustedIssuers)
            {
                if (Security.IsMatch(issuer, trustedIssuer))
                {
                    result = true;
                    break;
                }
            }
            if (!result)
            {
                using (ServerCertificateError dialog = new ServerCertificateError())
                {
                    dialog.SetErrorDetailsText(string.Concat("Организация: ", issuer));
                    if (dialog.ShowDialog() == DialogResult.OK)
                    {
                        string trustedIssuer = Security.GetHash(issuer);
                        _trustedIssuers.Add(trustedIssuer);
                        if (dialog.RememberAnswerChecked)
                        {
                            if (Settings.Default.TrustedIssuers == null)
                            {
                                Settings.Default.TrustedIssuers = new List<string>();
                            }
                            Settings.Default.TrustedIssuers.Add(trustedIssuer);
                            Settings.Default.Save();
                        }
                        result = true;
                    }
                    else
                    {
                        throw new UserCanceledException();
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Handles the Tick event of the Timer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void Timer_Tick(object sender, EventArgs e)
        {
            UpdateIncomingTrafficAmount();
        }

        /// <summary>
        /// Handles the Click event of the NotificationMenuGetDataNow control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void NotificationMenuGetDataNow_Click(object sender, EventArgs e)
        {
            UpdateIncomingTrafficAmount();
        }

        /// <summary>
        /// Updates the incoming traffic amount.
        /// </summary>
        private void UpdateIncomingTrafficAmount()
        {
            bool trafficAmountUpdated = false;
            do
            {
                try
                {
                    trafficAmountUpdated = true;
                    string incomingTrafficAmount = GetIncomingTrafficAmount();
                    ShowNotification(incomingTrafficAmount);
                }
                catch (UserCanceledException)
                {
                    ShowNotification("Отменено пользователем.");
                }
                catch (WebAuthenticationFailedException)
                {
                    _userName = string.Empty;
                    _password = string.Empty;
                    trafficAmountUpdated = false;
                }
                catch (ProxyAuthenticationFailedException)
                {
                    _proxyUserName = string.Empty;
                    _proxyPassword = string.Empty;
                    trafficAmountUpdated = false;
                }
            } while (!trafficAmountUpdated);
        }

        /// <summary>
        /// Shows the notification.
        /// </summary>
        /// <param name="text">The text.</param>
        private void ShowNotification(string text)
        {
            _notification.Text = text;
            _notification.BalloonTipText = text;
            _notification.BalloonTipTitle = text;
            _notification.BalloonTipIcon = ToolTipIcon.Info;
            _notification.ShowBalloonTip(Settings.Default.ToolTipDelayTimeout);
        }

        /// <summary>
        /// Handles the Click event of the Exit menu item of the notification icon context menu strip control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void NotificationMenuExit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        /// <summary>
        /// Gets the incoming traffic amount.
        /// </summary>
        /// <returns>Returns the amount of the incoming traffic.</returns>
        private string GetIncomingTrafficAmount()
        {
            HttpWebRequest request = AuthenticateWebUser();
            request = InitializeDetailedViewRequest(request);
            HttpWebResponse response = GetResponse(request);
            string responseHtml = new StreamReader(response.GetResponseStream()).ReadToEnd();
            Match match = Regex.Match(HttpUtility.HtmlDecode(responseHtml), "[\\d,.]+\\s*mb", RegexOptions.IgnoreCase);
            string result = null;
            if (match.Success)
            {
                result = match.Value;
            }
            return result;
        }

        /// <summary>
        /// Gets the response.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>The response.</returns>
        private static HttpWebResponse GetResponse(HttpWebRequest request)
        {
            HttpWebResponse response;
            try
            {
                response = request.GetResponse() as HttpWebResponse;
            }
            catch (WebException ex)
            {
                CheckWebExceptionIsNotProxyAuthenticationFailed(ex);
                throw;
            }
            return response;
        }

        /// <summary>
        /// Checks the web exception is not proxy authentication failed.
        /// </summary>
        /// <param name="ex">The ex.</param>
        private static void CheckWebExceptionIsNotProxyAuthenticationFailed(WebException ex)
        {
            string exceptionDetails = ex.ToString();
            if (Regex.IsMatch(exceptionDetails, ".*\\(407\\) Proxy Authentication Required.*", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
            {
                throw new ProxyAuthenticationFailedException();
            }
        }

        /// <summary>
        /// Initializes the detailed view request.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>The request to the detailed view.</returns>
        private HttpWebRequest InitializeDetailedViewRequest(HttpWebRequest request)
        {
            HttpWebResponse response = GetResponse(request);
            string responseHtml = new StreamReader(response.GetResponseStream()).ReadToEnd();
            string singleQuoteMatchGroupName = "singleQuote";
            string doubleQuoteMatchGroupName = "doubleQuote";
            string linkText = DateTime.Now.ToString(Settings.Default.LinkDateFormat, CultureInfo.InvariantCulture);
            string replacePattern = string.Format(
                CultureInfo.InvariantCulture,
                "(?<=<a[^>]*href=)(?:(?<{0}>')|(?<{1}>\"))?(?({0})[^']|(?({1})[^\"]|[^\\s>]))*(?({0})\\k<{0}>|(?({1})\\k<{1}>|\\s))(?=[^>]*>\\s*{2}\\s*</a>)",
                singleQuoteMatchGroupName,
                doubleQuoteMatchGroupName,
                linkText);
            Regex regex = new Regex(replacePattern, RegexOptions.IgnoreCase);
            Match match = regex.Match(responseHtml);
            HttpWebRequest result = null;
            if (match.Success)
            {
                string relativeUrl = HttpUtility.HtmlDecode(match.Value);
                if (match.Groups[singleQuoteMatchGroupName].Success)
                {
                    relativeUrl = relativeUrl.Replace(match.Groups[singleQuoteMatchGroupName].Value, string.Empty);
                }
                else if (match.Groups[doubleQuoteMatchGroupName].Success)
                {
                    relativeUrl = relativeUrl.Replace(match.Groups[doubleQuoteMatchGroupName].Value, string.Empty);
                }
                Uri originalUrl = request.RequestUri;
                string absoluteUrl = string.Concat(originalUrl.Scheme, "://", originalUrl.Host, "/", relativeUrl);
                result = CreateHttpWebRequest(absoluteUrl);
                result.Headers[HttpRequestHeader.Cookie] = request.Headers[HttpRequestHeader.Cookie];
            }
            return result;
        }

        /// <summary>
        /// Creates the HTTP web request.
        /// </summary>
        /// <param name="url">The URL.</param>
        /// <returns>Initialized HTTP web request.</returns>
        private HttpWebRequest CreateHttpWebRequest(string url)
        {
            HttpWebRequest result = WebRequest.Create(url) as HttpWebRequest;
            Uri siteUrl = new Uri(url);
            Uri proxyUrl = result.Proxy.GetProxy(siteUrl);
            if (proxyUrl != siteUrl)
            {
                if (string.IsNullOrEmpty(_proxyUserName) || string.IsNullOrEmpty(_proxyPassword))
                {
                    using (LoginForm dialog = new LoginForm())
                    {
                        dialog.Text = "Авторизация прокси";
                        if (dialog.ShowDialog() == DialogResult.OK)
                        {
                            _proxyUserName = dialog.UserName;
                            _proxyPassword = dialog.Password;
                            if (dialog.RememberMeChecked)
                            {
                                Settings.Default.ProxyUserName = Security.EncryptValue(_proxyUserName);
                                Settings.Default.ProxyPassword = Security.EncryptValue(_proxyPassword);
                                Settings.Default.Save();
                            }
                        }
                        else
                        {
                            throw new UserCanceledException();
                        }
                    }
                }
                result.Proxy.Credentials = new NetworkCredential(_proxyUserName, _proxyPassword);
            }
            return result;
        }

        /// <summary>
        /// Joins the cookies for the request.
        /// </summary>
        /// <param name="cookies">The cookies.</param>
        /// <returns>Joined cookies.</returns>
        private static string JoinCookiesForRequest(List<string> cookies)
        {
            StringBuilder result = new StringBuilder();
            foreach (string cookie in cookies)
            {
                if (result.Length != 0)
                {
                    result.Append("; ");
                }
                result.Append(cookie);
            }
            return result.ToString();
        }

        /// <summary>
        /// Splites the cookies from response.
        /// </summary>
        /// <param name="cookies">The cookies.</param>
        /// <returns>List of cookies.</returns>
        private static List<string> SplitCookiesFromResponse(string cookies)
        {
            string cookieValueGroupName = "cookieValue";
            string pattern = string.Format(CultureInfo.InvariantCulture, "(?:^|,)(?<{0}>[^;]*)(?:;|$)", cookieValueGroupName);
            MatchCollection matches = Regex.Matches(cookies, pattern);
            List<string> result = new List<string>();
            foreach (Match match in matches)
            {
                if (match.Success)
                {
                    Group cookieValueGroup = match.Groups[cookieValueGroupName];
                    if (cookieValueGroup.Success)
                    {
                        result.Add(cookieValueGroup.Value);
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Authenticates this instance.
        /// </summary>
        /// <returns>Next web request with prepared cookies.</returns>
        private HttpWebRequest AuthenticateWebUser()
        {
            if (string.IsNullOrEmpty(_userName) || string.IsNullOrEmpty(_password))
            {
                UpdateWebUserCredentials();
            }
            HttpWebRequest request = CreateHttpWebRequest(Settings.Default.LoginUrl);
            request.AllowAutoRedirect = false;
            byte[] data = Encoding.UTF8.GetBytes(string.Concat("LOGIN=", _userName, "&PASSWD=", _password, "&URL=elportal.vtt.net"));
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = data.Length;
            try
            {
                request.GetRequestStream().Write(data, 0, data.Length);
            }
            catch (WebException ex)
            {
                CheckWebExceptionIsNotProxyAuthenticationFailed(ex);
                throw;
            }
            HttpWebResponse response = GetResponse(request);
            HttpWebRequest result = CreateHttpWebRequest(response.Headers[HttpResponseHeader.Location]);
            List<string> responeCookies = SplitCookiesFromResponse(response.Headers[HttpResponseHeader.SetCookie]);
            if (responeCookies.Count < 2)
            {
                throw new WebAuthenticationFailedException();
            }
            result.Headers[HttpRequestHeader.Cookie] = JoinCookiesForRequest(responeCookies);
            return result;
        }

        /// <summary>
        /// Updates the web user credentials.
        /// </summary>
        private void UpdateWebUserCredentials()
        {
            using (LoginForm dialog = new LoginForm())
            {
                dialog.Text = "Авторизация на веб-сайте";
                if (dialog.ShowDialog() == DialogResult.OK)
                {
                    _userName = dialog.UserName;
                    _password = dialog.Password;
                    if (dialog.RememberMeChecked)
                    {
                        Settings.Default.UserName = Security.EncryptValue(_userName);
                        Settings.Default.Password = Security.EncryptValue(_password);
                        Settings.Default.Save();
                    }
                }
                else
                {
                    throw new UserCanceledException();
                }
            }
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (_notification != null)
            {
                _notification.Dispose();
                _notification = null;
            }
            if (_timer != null)
            {
                _timer.Dispose();
            }
        }
        #endregion
    }
}
